disk: *Disk,
// host endian
number: u16,
type: FileType,
nlink: u16 = 1,
size: u32 = 0,
addrs: [@typeInfo(@FieldType(Dinode, "addrs")).array.len]Block = @splat(.zero),

const Inode = @This();
const Disk = @import("Disk.zig");
const std = @import("std");
const assert = std.debug.assert;

const fs = @import("fs");
const Dinode = fs.Inode.Dinode;
const Dirent = fs.Inode.Dirent;
const FileType = fs.Inode.Type;
const Block = ForeignInt(u32);

const root = @import("root");
const inodestart = root.inodestart;

/// Converts a value between native byte order and target byte order.
///
/// This is a bidirectional converter that can be used to:
/// - Convert from native byte order to target byte order when writing data
/// - Convert from target byte order back to native byte order when reading data
///
/// The conversion automatically handles the endianness of both the host system
/// and the target architecture specified in the build configuration.
fn convert(T: type, value: T) T {
    return std.mem.nativeTo(T, value, root.target_arch.endian());
}

/// Wrapper type for storing values in foreign-endian format with safe conversion.
///
/// This creates an extern struct that maintains a value in target (foreign) byte order,
/// while providing type-safe conversion methods to/from native byte order.
fn ForeignInt(T: type) type {
    return extern struct {
        /// The actual value stored in foreign-endian format
        int: T,

        // Compile-time validation that our wrapper doesn't add any padding
        comptime {
            assert(@sizeOf(Self) == @sizeOf(T));
        }

        const Self = @This();
        pub const zero: Self = .init(0);

        pub fn init(value: T) Self {
            return .{
                .int = convert(T, value),
            };
        }

        /// Get the value converted to native-endian format
        pub fn get(self: Self) T {
            return convert(T, self.int);
        }

        /// Set the value from a native-endian input
        pub fn set(self: *Self, value: T) void {
            self.int = convert(T, value);
        }
    };
}

/// Allocates a new inode on the disk with the specified file type.
/// Caller must call commit() to persist it to disk.
pub fn alloc(disk: *Disk, @"type": FileType) Inode {
    defer disk.freeinode += 1;
    return .{
        .disk = disk,
        .number = disk.freeinode,
        .type = @"type",
    };
}

/// Persists inode metadata to disk.
/// Calculates sector/offset automatically. Panics if write fails.
pub fn commit(inode: Inode) void {
    const sector = inode.number / fs.IPB + inodestart;
    const offset = (inode.number % fs.IPB) * @sizeOf(Dinode);
    _ = inode.disk.write(sector, offset, std.mem.asBytes(&inode.dinode())) catch |err|
        @panic(@errorName(err));
}

/// Converts an in-memory Inode to an on-disk Dinode format.
fn dinode(self: Inode) Dinode {
    return .{
        .type = @enumFromInt(convert(u16, @intFromEnum(self.type))),
        .nlink = convert(u16, self.nlink),
        .size = convert(u32, self.size),
        .addrs = @bitCast(self.addrs),
    };
}

/// Appends target-endian data to file, allocating blocks as needed.
pub fn append(inode: *Inode, buf: []const u8) !void {
    var n: usize = 0;
    var filesize = inode.size;

    while (n < buf.len) {
        const fbn = filesize / fs.BSIZE;
        assert(fbn < fs.MAXFILE);

        const block = try inode.bitmap(fbn);
        const offset = filesize - (fbn * fs.BSIZE);
        const nbytes = try inode.disk.write(block, offset, buf[n..]);

        filesize += @intCast(nbytes);
        n += nbytes;
    }

    inode.size = filesize;
}

/// Adds a directory entry to the specified inode.
pub fn entry(inode: *Inode, inum: u16, path: []const u8) !void {
    assert(path.len <= Dirent.DIRSIZ);
    const dir_entry: Dirent = .{
        .inum = convert(u16, inum),
        .name = .init(path),
    };
    try inode.append(std.mem.asBytes(&dir_entry));
}

/// Return the disk block address of the nth block in inode.
/// If there is no such block, bitmap allocates one.
fn bitmap(inode: *Inode, block_number: usize) !usize {
    const disk = inode.disk;

    const idx: [3]?usize = fs.blockidx(@intCast(block_number));
    const idx0 = idx[0].?;
    if (inode.addrs[idx0].get() == 0) {
        inode.addrs[idx0].set(disk.freeblock);
        disk.freeblock += 1;
    }

    // indirect block
    if (idx[1]) |idx1| {
        var singly_blk: [fs.blocks_per_indirect]Block = undefined;
        if (inode.addrs[idx0].get() != 0) {
            try disk.read(inode.addrs[idx0].get(), @ptrCast(&singly_blk));
        }

        if (singly_blk[idx1].get() == 0) {
            singly_blk[idx1].set(disk.freeblock);
            disk.freeblock += 1;
            _ = try disk.write(inode.addrs[idx0].get(), idx1 * @sizeOf(u32), std.mem.asBytes(&singly_blk[idx1]));
        }

        // doubly-indirect block
        if (idx[2]) |idx2| {
            var doubly_blk: [fs.blocks_per_indirect]Block = undefined;
            if (singly_blk[idx1].get() != 0) {
                try disk.read(singly_blk[idx1].get(), @ptrCast(&doubly_blk));
            }

            if (doubly_blk[idx2].get() == 0) {
                doubly_blk[idx2].set(disk.freeblock);
                disk.freeblock += 1;
                _ = try disk.write(singly_blk[idx1].get(), idx2 * @sizeOf(u32), std.mem.asBytes(&doubly_blk[idx2]));
            }

            return doubly_blk[idx2].get();
        }

        return singly_blk[idx1].get();
    }

    return inode.addrs[idx0].get();
}
